import { fetchChannelnfo } from "./thumbnailData";
import { VideoID } from "../../maze-utils/src/video";
import { getNumberOfThumbnailCacheRequests, getThumbnailUrl, getVideoThumbnailIncludingUnsubmitted, isActiveThumbnailCacheRequest, isFetchingFromThumbnailCache, queueThumbnailCacheRequest, waitForThumbnailCache } from "../dataFetching";
import { log, logError } from "../utils/logger";
import { BrandingLocation, ShowCustomBrandingInfo, getActualShowCustomBranding, shouldShowCasual } from "../videoBranding/videoBranding";
import { isFirefoxOrSafari, timeoutPomise, waitFor } from "../../maze-utils/src";
import Config, { ThumbnailFallbackOption } from "../config/config";
import { getThumbnailFallbackAutoGeneratedOption, getThumbnailFallbackOption, shouldReplaceThumbnails, shouldReplaceThumbnailsFastCheck } from "../config/channelOverrides";
import { countThumbnailReplacement } from "../config/stats";
import { ThumbnailCacheOption } from "../config/config";
import { getThumbnailImageSelectors, getThumbnailSelectors } from "../../maze-utils/src/thumbnail-selectors";
import { isOnV3Extension, onMobile } from "../../maze-utils/src/pageInfo";
import { MobileFix, addNodeToListenFor } from "../utils/titleBar";
import { resetMediaSessionThumbnail, setMediaSessionThumbnail } from "../videoBranding/mediaSessionHandler";
import { isSafari } from "../../maze-utils/src/config";
import { RenderedThumbnailVideo, ThumbnailVideo, thumbnailDataCache } from "./thumbnailDataCache";
import { isOnCorrectVideo } from "../titles/titleRenderer";
import * as CompileConfig from "../../config.json";
import { fetchVideoMetadata, Format, getPlaybackFormats, isLiveOrUpcoming } from "../../maze-utils/src/metadataFetcher";

const activeRendersMax = isFirefoxOrSafari() ? 3 : 6;
const activeRenders: Record<VideoID, Promise<RenderedThumbnailVideo | null>> = {};
const renderQueue: Array<() => void> = [];
const renderQueueCallbacks: Record<VideoID, Array<() => void>> = {};

function waitForSpotInRenderQueue(videoID: VideoID): Promise<void> {
    if (Object.keys(activeRenders).length >= activeRendersMax) {
        return new Promise((resolve) => {
            renderQueue.push(resolve);
            renderQueueCallbacks[videoID] ??= [];
            renderQueueCallbacks[videoID].push(resolve);
        });
    }

    return Promise.resolve();
}

function nextInRenderQueue() {
    renderQueue.shift()?.();
}

export function thumbnailCacheDownloaded(videoID: VideoID): void {
    const callbacks = renderQueueCallbacks[videoID];
    if (callbacks) {
        for (const callback of callbacks) {
            callback();
        }

        delete renderQueueCallbacks[videoID];
    }
}

const thumbnailRendererControls: Record<VideoID, Array<(error?: string) => void>> = {};

function stopRendering(videoID: VideoID, error?: string) {
    const control = thumbnailRendererControls[videoID];
    if (control) {
        for (const callback of control) {
            callback(error);
        }
    }

    delete thumbnailRendererControls[videoID];
}

function addStopRenderingCallback(videoID: VideoID, callback: (error?: string) => void) {
    thumbnailRendererControls[videoID] ??= [];
    thumbnailRendererControls[videoID].push(callback);
}

export class ThumbnailNotInCacheError extends Error {
    constructor(videoID: VideoID) {
        super(`Thumbnail not found in cache for ${videoID}`);
    }
}

export async function renderThumbnail(videoID: VideoID, width: number,
    height: number, saveVideo: boolean, timestamp: number, onlyFromThumbnailCache = false, ignoreTimeout = false): Promise<RenderedThumbnailVideo | null> {

    const startTime = performance.now();

    if (onlyFromThumbnailCache) {
        await waitForThumbnailCache(videoID);
    }

    let bestVideoData = await findBestVideo(videoID, width, height, timestamp);
    if (bestVideoData.renderedThumbnail) {
        return bestVideoData.renderedThumbnail;
    }

    if (onlyFromThumbnailCache) {
        throw new ThumbnailNotInCacheError(videoID);
    }
    
    await Promise.race([
        waitForSpotInRenderQueue(videoID),
        timeoutPomise(Config.config!.renderTimeout - (performance.now() - startTime)).catch(() => ({}))
    ]);
    delete renderQueueCallbacks[videoID];

    if (!ignoreTimeout && performance.now() - startTime > Config.config!.renderTimeout
            && !isCachedThumbnailLoaded(videoID, timestamp)) {
        return null;
    }

    bestVideoData = await findBestVideo(videoID, width, height, timestamp);
    if (bestVideoData.renderedThumbnail) {
        return bestVideoData.renderedThumbnail;
    }
    const reusedVideo: HTMLVideoElement | null = bestVideoData.video ?? null;

    // eslint-disable-next-line no-async-promise-executor, @typescript-eslint/no-misused-promises
    return await new Promise(async (resolve, reject) => {
        const start = Date.now();
        const stopCallbackHandler = new Promise<string | undefined>((resolve) => {
            addStopRenderingCallback(videoID, resolve);
        });
        const format = await Promise.race([getPlaybackFormats(videoID, width, height), stopCallbackHandler]);
        const videoCache = thumbnailDataCache.setupCache(videoID);
        if (!format || !(format as Format)?.url) {
            handleThumbnailRenderFailure(videoID, width, height, timestamp, resolve);
            return;
        }
        if (typeof(format) === "string") {
            resolve(videoCache.video.find(v => v.timestamp === timestamp && v.rendered && v.fromThumbnailCache) as RenderedThumbnailVideo ?? null);
            return;
        }
        
        const video = createVideo(reusedVideo, format.url, timestamp);
        const videoCacheObject: ThumbnailVideo = {
            video: video,
            width: format.width,
            height: format.height,
            rendered: false,
            onReady: [resolve],
            timestamp
        };
        videoCache.video.push(videoCacheObject);

        let resolved = false;
        let videoLoadedTimeout: NodeJS.Timeout | null = null;

        const clearVideo = (saveVideo: boolean) => {
            video.removeEventListener("error", errorHandler);
            video.removeEventListener("loadeddata", loadedData); // eslint-disable-line @typescript-eslint/no-misused-promises
            video.removeEventListener("seeked", loadedData) // eslint-disable-line @typescript-eslint/no-misused-promises

            if (!saveVideo) {
                video.removeAttribute("src");
                video.load();
                video.remove();
            }
        };

        const loadedData = async () => {
            const betterVideo = thumbnailDataCache.getFromCache(videoID)?.video?.find(v => v.width >= width && v.height >= height
                && v.timestamp === timestamp && v.rendered);
            if (betterVideo) {
                video.remove();
                resolved = true;

                reject("Already rendered");
                return;
            }

            log(videoID, "videoLoaded", video.currentTime, video.readyState, video.seeking, format)
            if (video.readyState < 2 || video.seeking || video.currentTime !== timestamp) {
                if (videoLoadedTimeout) clearTimeout(videoLoadedTimeout);

                if (video.seeking) {
                    video.addEventListener("seeked", loadedData, { once: true }); // eslint-disable-line @typescript-eslint/no-misused-promises
                } else {
                    videoLoadedTimeout = setTimeout(loadedData, 50); // eslint-disable-line @typescript-eslint/no-misused-promises
                }

                return;
            }

            const blobResult = await renderToBlob(video);
            if (blobResult === null) {
                errorHandler();
                return;
            }

            if (!chrome.runtime?.id) {
                // Extension context has been invalidated, just give up;
                errorHandler();
                return;
            }

            const videoInfo: RenderedThumbnailVideo = {
                blob: blobResult,
                video: saveVideo ? video : null,
                width: video.videoWidth,
                height: video.videoHeight,
                rendered: true,
                onReady: [],
                timestamp,
                fromThumbnailCache: false
            };

            const videoCache = thumbnailDataCache.setupCache(videoID);
            const currentVideoInfoIndex = videoCache.video.findIndex(v => v.width === video.videoWidth
                && v.height === video.videoHeight && v.timestamp === timestamp);
            const currentVideoInfo = currentVideoInfoIndex !== -1 ? videoCache.video[currentVideoInfoIndex] : null;
            if (currentVideoInfo) {
                for (const callback of currentVideoInfo.onReady) {
                    callback(videoInfo);
                }

                videoCache.video[currentVideoInfoIndex] = videoInfo;
            } else {
                videoCache.video.push(videoInfo);
            }

            log(videoID, (Date.now() - start) / 1000, width > 0 ? "full" : "smaller");

            clearVideo(saveVideo);
            resolved = true;
        };

        const errorHandler = () => void (() => {
            if (!resolved) {
                if (videoLoadedTimeout) clearTimeout(videoLoadedTimeout);

                clearVideo(false);

                if (chrome.runtime?.id) {
                    // Make sure extension context is still valid
                    handleThumbnailRenderFailure(videoID, width, height, timestamp, resolve);
                }
            }
        })();

        video.addEventListener("error", errorHandler);
        if (reusedVideo) {
            video.addEventListener("seeked", loadedData); // eslint-disable-line @typescript-eslint/no-misused-promises
        } else {
            video.addEventListener("loadeddata", loadedData); // eslint-disable-line @typescript-eslint/no-misused-promises
        }

        // Give up after some times
        setTimeout(() => {
            if (!resolved) {
                errorHandler();
            }
        }, Config.config!.renderTimeout - (performance.now() - startTime));

        addStopRenderingCallback(videoID, () => {
            resolved = true;
            clearVideo(false);

            bestVideoData = findBestVideo(videoID, width, height, timestamp);
            if (bestVideoData.renderedThumbnail) {
                resolve(bestVideoData.renderedThumbnail);
            }
        });
    });
}

interface BestVideoData {
    renderedThumbnail?: Promise<RenderedThumbnailVideo | null>;
    video?: HTMLVideoElement | null;
}

function findBestVideo(videoID: VideoID, width: number, height: number, timestamp: number): BestVideoData {
    const existingCache = thumbnailDataCache.getFromCache(videoID);

    if (existingCache && existingCache?.video?.length > 0) {
        const bestVideos = ((width && height) ? existingCache.video.filter(v => (v.rendered && v.fromThumbnailCache)
                || (v.width >= width && v.height >= height))
            : existingCache.video).sort((a, b) => b.width - a.width).sort((a, b) => +b.rendered - +a.rendered);

        const sameTimestamp = bestVideos.find(v => v.timestamp === timestamp || v.timestamp.toFixed(3) === timestamp.toFixed(3));

        if (sameTimestamp?.rendered) {
            return {
                renderedThumbnail: Promise.resolve(sameTimestamp)
            };
        } else if (sameTimestamp) {
            return {
                renderedThumbnail: new Promise((resolve) => {
                    sameTimestamp.onReady.push((a) => {
                        resolve(a)
                    });
                })
            };
        } else if (bestVideos.length > 0) {
            return {
                video: bestVideos[0].video
            };
        }
    }

    return {};
}

function handleThumbnailRenderFailure(videoID: VideoID, width: number, height: number,
        timestamp: number, resolve: (video: RenderedThumbnailVideo | null) => void): void {
    const videoCache = thumbnailDataCache.setupCache(videoID);
    const thumbnailFailed = !!videoCache.thumbnailCachesFailed?.has?.(timestamp);
    const listeners = [resolve];

    if (videoCache.video) {
        const filter = v => v.width !== width && v.height !== height && v.timestamp !== timestamp;
        const removedItems = videoCache.video.filter((v) => !filter(v));
        videoCache.video = videoCache.video.filter(filter);

        listeners.push(...removedItems.flatMap((v) => v.onReady));
    }

    if (!thumbnailFailed && Config.config!.thumbnailCacheUse !== ThumbnailCacheOption.Disable && !CompileConfig.debug) {
        // Force the thumbnail to be generated by the server
        // Generate it one-by-one because of how callbacks are handled
        (async () => {
            let tries = 0;
            while (isActiveThumbnailCacheRequest(videoID) && tries < 10) {
                await waitForThumbnailCache(videoID);
                tries++;
            }

            queueThumbnailCacheRequest(videoID, timestamp, undefined, false, true)
        })().catch(logError);

        videoCache.failures.push({
            timestamp,
            onReady: [...listeners]
        });
    } else {
        for (const callback of listeners) {
            callback(null);
        }
    }
}

function renderToBlob(surface: HTMLVideoElement | HTMLCanvasElement): Promise<Blob | null> {
    let deleteSurface = false;
    if (surface instanceof HTMLVideoElement) {
        const canvas = document.createElement("canvas");
        canvas.width = surface.videoWidth;
        canvas.height = surface.videoHeight;
        const context = canvas.getContext("2d")!;
        context.drawImage(surface, 0, 0);

        if (isFirefoxOrSafari() 
            && !isSafari()
            && context.getImageData(0, 0, 1, 1).data[3] === 0) {
            // Firefox has a bug and has failed to render this
            return Promise.resolve(null);
        }

        surface = canvas;
        deleteSurface = true;
    }

    return new Promise((resolve, reject) => {
        (surface as HTMLCanvasElement).toBlob((blob) => {
            if (deleteSurface) {
                surface.remove();
            }
            if (blob) {
                resolve(blob);
            } else {
                reject("Failed to create blob");
            }
        }, "image/webp", 1);
    });
}

/**
 * Returns a canvas that will be drawn to once the thumbnail is ready.
 * 
 * Starts with lower resolution and replaces it with higher resolution when ready.
 */
export async function createThumbnailImageElement(existingElement: HTMLImageElement | null, videoID: VideoID, width: number,
    height: number, brandingLocation: BrandingLocation, forcedTimestamp: number | null,
    saveVideo: boolean, stillValid: () => Promise<boolean>, ready: (image: HTMLImageElement, url: string, timestamp: number) => unknown,
    failure: () => unknown, showOriginal: () => unknown): Promise<HTMLImageElement | null> {

    const image = existingElement ?? document.createElement("img");
    image.style.display = "none";

    let timestamp = forcedTimestamp as number;
    let isRandomTime = false;
    if (timestamp === null) {
        try {
            const thumbnailPromise = getVideoThumbnailIncludingUnsubmitted(videoID, brandingLocation);
            // Wait for whatever is first
            await Promise.race([
                thumbnailPromise,
                shouldReplaceThumbnails(videoID)
            ]);

            if (shouldReplaceThumbnailsFastCheck(videoID) === false) {
                showOriginal();
                return null;
            }

            // Will keep waiting for the thumbnail if the channel check finished first
            const thumbnail = await thumbnailPromise;
            if (thumbnail && !thumbnail.original) {
                timestamp = thumbnail.timestamp;
                isRandomTime = thumbnail.isRandomTime;
            } else if (!thumbnail
                    && [ThumbnailFallbackOption.AutoGenerated, ThumbnailFallbackOption.RandomTime].includes(await getThumbnailFallbackOption(videoID))) {
                failure();
                return image;
            } else {
                // Original thumbnail will be shown automatically
                return null;
            }
        } catch (e) {
            return null;
        }
    }

    let waitingForFetchTries = 0;
    while (isFetchingFromThumbnailCache(videoID, timestamp)) {
        // Wait for the thumbnail to be fetched from the cache before trying local generation
        try {
            const promises = [
                waitForThumbnailCache(videoID),
                timeoutPomise(Config.config!.startLocalRenderTimeout).catch(() => ({}))
            ];

            // If we know about it, we don't want to return early without using the timeout
            if (shouldReplaceThumbnailsFastCheck(videoID) === null) {
                promises.push(shouldReplaceThumbnails(videoID));
            }

            await Promise.race(promises);

            if (isFetchingFromThumbnailCache(videoID, timestamp) 
                    && getNumberOfThumbnailCacheRequests() > 3 && waitingForFetchTries < 5
                    && shouldReplaceThumbnailsFastCheck(videoID) !== false) {
                waitingForFetchTries++;
                log(videoID, "Lots of thumbnail cache requests in progress, waiting a little longer");
            } else {
                break;
            }
        } catch (e) {
            // Go on and do a local render
            break;
        }
    }

    if (!await stillValid()) {
        return null;
    }

    const result = async (canvasInfo: RenderedThumbnailVideo | null) => {
        if (!await stillValid()) {
            return;
        }

        if (!canvasInfo) {
            failure();
            return;
        }

        const url = URL.createObjectURL(canvasInfo.blob);
        ready(image, url, timestamp);
    }

    const activeRender = activeRenders[videoID] ?? renderThumbnail(videoID, width, height, saveVideo, timestamp, isRandomTime).finally(() => {
        delete activeRenders[videoID];
        nextInRenderQueue();
    });
    activeRenders[videoID] = activeRender;
    
    activeRender.then(result).catch((e) => {
        if (e instanceof ThumbnailNotInCacheError) {
            failure();
        } else {
            // Try again with lower resolution
            renderThumbnail(videoID, 0, 0, saveVideo, timestamp, isRandomTime).then(result).catch((e) => {
                log(`Failed to render thumbnail for ${videoID} due to ${e}`);
    
                failure();
            });
        }
    });

    return image;
}

export function drawCenteredToCanvas(canvas: HTMLCanvasElement, width: number, height: number,
    originalWidth: number, originalHeight: number, originalSurface: HTMLVideoElement | HTMLCanvasElement | ImageBitmap): void {
    const calculateWidth = height * originalWidth / originalHeight;
    const context = canvas.getContext("2d")!;
    
    context.drawImage(originalSurface, (width - calculateWidth) / 2, 0, calculateWidth, height);
}

function createVideo(existingVideo: HTMLVideoElement | null, url: string, timestamp: number): HTMLVideoElement {
    if (timestamp === 0 && !isFirefoxOrSafari()) timestamp += 0.001;
    
    const video = existingVideo ?? document.createElement("video");
    video.crossOrigin = "anonymous";
    // https://stackoverflow.com/a/69074004
    if (!existingVideo) video.src = `${url}#t=${timestamp}-${timestamp + 0.001}`;
    video.controls = false;
    video.volume = 0;
    if (isFirefoxOrSafari() && !isSafari()) {
        // Firefox has a bug where video will report as black until played at least once
        void video.play().then(() => {
            video.pause();
            video.currentTime = timestamp;
        });
    } else {
        video.pause();
        video.currentTime = timestamp;
    }

    return video;
}

export function getThumbnailImageSelector(brandingLocation: BrandingLocation): string {
    switch (brandingLocation) {
        case BrandingLocation.Related:
            return getThumbnailImageSelectors();
        case BrandingLocation.Endcards:
            return ".ytp-ce-covering-image";
        case BrandingLocation.Autoplay:
        case BrandingLocation.EndAutonav:
            return "div.ytp-autonav-endscreen-upnext-thumbnail";
        case BrandingLocation.EndRecommendations:
            return "div.ytp-videowall-still-image, div.ytp-modern-videowall-still-image";
        case BrandingLocation.EmbedSuggestions:
            return ".ytp-suggestion-image";
        case BrandingLocation.Watch:
        case BrandingLocation.ChannelTrailer:
            return ".ytp-cued-thumbnail-overlay-image";
        case BrandingLocation.UpNextPreview:
            return ".ytp-tooltip-bg";
        case BrandingLocation.Notification:
        case BrandingLocation.NotificationTitle:
            return ".thumbnail-container img";
        default:
            throw new Error("Invalid branding location");
    }
}

function getThumbnailBox(image: HTMLElement, brandingLocation: BrandingLocation): HTMLElement {
    switch (brandingLocation) {
        case BrandingLocation.Related:
            if (!onMobile()) {
                return image.closest([
                    getThumbnailSelectors(":not([hidden])"),
                    "ytd-hero-playlist-thumbnail-renderer"]
                .join(", ")) as HTMLElement;
            } else {
                return image;
            }
        case BrandingLocation.Autoplay:
        case BrandingLocation.UpNextPreview:
        case BrandingLocation.EndAutonav:
            return image;
        default:
            return image.parentElement!;
    }
}

/**
 * Applies desaturation effect to the supplied thumbnail.
 *
 * @param {HTMLImageElement | HTMLElement} thumbnail - The HTML image element or HTML element representing the thumbnail.
 */
function applyThumbnailDesaturation(thumbnail: HTMLImageElement | HTMLElement) {
    thumbnail.style.filter = `grayscale(${((100 - Config.config!.thumbnailSaturationLevel) / 100)})`;
}

export async function replaceThumbnail(element: HTMLElement, videoID: VideoID, brandingLocation: BrandingLocation,
        showCustomBranding: ShowCustomBrandingInfo, timestamp?: number): Promise<boolean> {
    const thumbnailSelector = getThumbnailImageSelector(brandingLocation);
    const image = await waitFor(() => element.querySelector(thumbnailSelector) as HTMLImageElement).catch(() => null);
    if (!image) {
        // Could be video with autoplay, so no thumbnail
        return false;
    }

    const box = getThumbnailBox(image, brandingLocation);

    if (Config.config!.extensionEnabled) {
        applyThumbnailDesaturation(image)
    } else {
        image.style.removeProperty("filter")
    }

    if (showCustomBranding.knownValue === false || !Config.config!.extensionEnabled 
            || shouldReplaceThumbnailsFastCheck(videoID) === false
            || (Config.config!.showOriginalThumbWhenCasual && await shouldShowCasual(videoID, element, showCustomBranding, brandingLocation))) {
        resetToShowOriginalThumbnail(image, brandingLocation);

        if (Config.config!.extensionEnabled && await shouldReplaceThumbnails(videoID)) {
            // Still check if the thumbnail is supposed to be changed or not
            // If showing a live cover, it will be replaced even with no random timestamp stored
            const thumbnail = await getVideoThumbnailIncludingUnsubmitted(videoID, brandingLocation);
            return (!!thumbnail && !thumbnail.original) || !!await shouldShowLiveCover(videoID);
        } else {
            return false;
        }
    }

    if (image && box) {
        let objectWidth = box.offsetWidth;
        let objectHeight = box.offsetHeight;
        if (objectWidth === 0 || objectHeight === 0) {
            const style = window.getComputedStyle(box);
            objectWidth = parseInt(style.getPropertyValue("width").replace("px", ""), 10);
            objectHeight = parseInt(style.getPropertyValue("height").replace("px", ""), 10);

            if (objectWidth === 0) {
                try {
                    await waitFor(() => box.offsetWidth > 0 && box.offsetHeight > 0);
                } catch (e) {
                    // No need to render this thumbnail since it is hidden
                    return false;
                }
            }
        }

        const width = objectWidth * window.devicePixelRatio;
        const height = objectHeight * window.devicePixelRatio;

        if (Config.config!.hideDetailsWhileFetching) {
            image.style.display = "none";
            image.classList.remove("cb-visible");
            resetBackgroundColor(image, brandingLocation);
        } else {
            resetToShowOriginalThumbnail(image, brandingLocation);
        }

        const displayThumbnail = async (thumbnail: HTMLImageElement | HTMLElement, blobUrl: string | null, remoteUrl: string, removeWidth: boolean) => {
            if (blobUrl && thumbnail instanceof HTMLImageElement) thumbnail.src = blobUrl;

            if (!await getActualShowCustomBranding(showCustomBranding)) {
                return;
            }

            thumbnail!.style.removeProperty("display");
            if (Config.config!.extensionEnabled) {
                applyThumbnailDesaturation(thumbnail);
            } else {
                thumbnail.style.removeProperty("filter");
            }
            if (!(thumbnail instanceof HTMLImageElement) || thumbnail.complete) {
                if (removeWidth) thumbnail!.style.removeProperty("width");
                removeBackgroundColor(image, brandingLocation);
            } else {
                thumbnail.addEventListener("load", () => {
                    if (removeWidth) thumbnail!.style.removeProperty("width");
                    removeBackgroundColor(image, brandingLocation);
                }, { once: true });
            }

            if (brandingLocation === BrandingLocation.Related) {
                box.setAttribute("loaded", "");
            }

            if (brandingLocation === BrandingLocation.Watch
                    && Config.config!.thumbnailCacheUse === ThumbnailCacheOption.OnAllPages) {
                setMediaSessionThumbnail(remoteUrl);
            }

            countThumbnailReplacement(videoID);
        }

        let existingImageElement = image.parentElement?.querySelector(".cbCustomThumbnailCanvas") as HTMLImageElement | null;
        if (existingImageElement && existingImageElement.nodeName === "DIV") {
            // Get rid of it, this is a live cover
            existingImageElement.remove();
            existingImageElement = null;
        }

        try {
            const thumbnail = await createThumbnailImageElement(existingImageElement, videoID, width, height, brandingLocation, timestamp ?? null, false, () => {
                return isOnCorrectVideo(element, brandingLocation, videoID);
            }, async (thumbnail, url, timestamp) => {
                await displayThumbnail(thumbnail, url, getThumbnailUrl(videoID, timestamp), true);
            }, async () => {
                if (!await isLiveOrUpcoming(videoID) 
                        && [ThumbnailFallbackOption.RandomTime, ThumbnailFallbackOption.AutoGenerated].includes(await getThumbnailFallbackOption(videoID))) {
                    await resetToShowAutogenerated(videoID, thumbnail!, image, brandingLocation, displayThumbnail);
                } else if (await shouldShowLiveCover(videoID)) {
                    await resetToShowLiveCover(videoID, thumbnail!, image, brandingLocation, displayThumbnail);
                } else {
                    resetToShowOriginalThumbnail(image, brandingLocation);
                }
            }, () => {
                resetToShowOriginalThumbnail(image, brandingLocation);
            });

            getVideoThumbnailIncludingUnsubmitted(videoID, brandingLocation).then(async (thumbnailData) => {
                if (!await getActualShowCustomBranding(showCustomBranding)) {
                    return;
                }
    
                if (!thumbnailData || thumbnailData.original) {
                    if (!thumbnailData && await getThumbnailFallbackOption(videoID) === ThumbnailFallbackOption.Blank) {
                        resetToBlankThumbnail(image, brandingLocation);
                    } else if (!thumbnailData
                            && [ThumbnailFallbackOption.RandomTime, ThumbnailFallbackOption.AutoGenerated].includes(await getThumbnailFallbackOption(videoID))
                            && !await isLiveOrUpcoming(videoID)) {
                        await resetToShowAutogenerated(videoID, thumbnail!, image, brandingLocation, displayThumbnail);
                    } else if (thumbnailData?.original || !await shouldShowLiveCover(videoID)) {
                        // Will only get here if it did not create a random time thumbnail (such as innertube failing)
                        resetToShowOriginalThumbnail(image, brandingLocation);
                    }
                }
            }).catch(logError);
    
            if (!thumbnail) {
                // Hiding handled by already above then check
                return false;
            }

            // Waiting until now so that innertube has time to fetch data
            if (!await shouldReplaceThumbnails(videoID)) {
                resetToShowOriginalThumbnail(image, brandingLocation);

                return false;
            }

            if (!await getActualShowCustomBranding(showCustomBranding)) {
                resetToShowOriginalThumbnail(image, brandingLocation);

                // Still check if the thumbnail is supposed to be changed or not
                const thumbnail = await getVideoThumbnailIncludingUnsubmitted(videoID, brandingLocation);
                return !!thumbnail && !thumbnail.original;
            }

            image.style.display = "none";
            image.classList.remove("cb-visible");
            thumbnail.classList.add("style-scope");
            if (!isOnV3Extension()) {
                thumbnail.classList.add("ytd-img-shadow");
            } else {
                thumbnail.style.height = String(parseInt(image.getAttribute("width") ?? "0") * 9 / 16) + "px";
            }

            if (image.classList.contains("amsterdam-playlist-thumbnail")) {
                // Playlist header on mobile
                thumbnail.className = image.className;
            }

            // New shorts UI
            if (image.classList.contains("ShortsLockupViewModelHostThumbnail")) {
                thumbnail.classList.add("ShortsLockupViewModelHostThumbnail");
            }

            thumbnail.classList.add("cbCustomThumbnailCanvas");
            thumbnail.style.removeProperty("display");

            if (brandingLocation === BrandingLocation.EndRecommendations) {
                thumbnail.classList.add("ytp-videowall-still-image");
                thumbnail.classList.add("ytp-modern-videowall-still-image");
                thumbnail.style.marginLeft = "auto";
                thumbnail.style.marginRight = "auto";
            } else if (brandingLocation === BrandingLocation.Autoplay || brandingLocation === BrandingLocation.EndAutonav) {
                thumbnail.classList.add("ytp-autonav-endscreen-upnext-thumbnail");
            } else if (brandingLocation === BrandingLocation.UpNextPreview) {
                thumbnail.classList.add("ytp-tooltip-bg");
            }

            if (!isOnV3Extension()) {
                thumbnail.style.height = "100%";
            }

            if ([BrandingLocation.EmbedSuggestions, BrandingLocation.UpNextPreview].includes(brandingLocation)) {
                thumbnail.style.width = image.style.width;
                thumbnail.style.height = image.style.height;
            }

            if (onMobile() && !image.classList.contains("amsterdam-playlist-thumbnail")) {
                thumbnail.style.position = "absolute";
                thumbnail.style.top = "0";

                thumbnail.style.marginLeft = "auto";
                thumbnail.style.marginRight = "auto";
                thumbnail.style.left = "0";
                thumbnail.style.right = "0";
            } else if (image.classList.contains("amsterdam-playlist-thumbnail")) {
                // Playlist header on mobile
                thumbnail.style.removeProperty("height");
            }

            if (brandingLocation === BrandingLocation.Autoplay
                    || brandingLocation === BrandingLocation.EndAutonav
                    || brandingLocation === BrandingLocation.EndRecommendations) {
                // For autoplay, the thumbnail is placed inside the image div, which has the image as the background image
                // This is because hiding the entire div would hide the video duration
                image.prepend(thumbnail);
                image.style.removeProperty("display");
                image.classList.add("cb-visible");
            } else {
                if (!isOnV3Extension()) {
                    image.parentElement?.appendChild?.(thumbnail);
                } else {
                    image.parentElement?.prepend?.(thumbnail);
                }
            }

            // 2024 Oct UI needs to replacement setup
            if (onMobile() || image.parentElement?.classList?.contains("yt-thumbnail-view-model__image")) {
                addNodeToListenFor(thumbnail, MobileFix.Replace);
            }
        } catch (e) {
            logError(e);

            resetToShowOriginalThumbnail(image, brandingLocation);
            return false;
        }
    }

    return !!image;
}

async function shouldShowLiveCover(videoID: VideoID) {
    return await isLiveOrUpcoming(videoID)
        && [ThumbnailFallbackOption.RandomTime, ThumbnailFallbackOption.AutoGenerated].includes(await getThumbnailFallbackOption(videoID))
        && Config.config!.showLiveCover;
}

async function resetToShowAutogenerated(videoID: VideoID, thumbnail: HTMLImageElement,
        image: HTMLImageElement, brandingLocation: BrandingLocation,
        displayThumbnail: (thumbnail: HTMLImageElement, blobUrl: string, remoteUrl: string, removeWidth: boolean) => Promise<void>) {
    const url = await getAutogeneratedThumbnailUrl(videoID);
    thumbnail.style.width = "100%";

    // Ensure errors default back to the original thumbnail
    const onError = () => {
        thumbnail.style.display = "none";
        thumbnail.removeAttribute("src");
        resetToShowOriginalThumbnail(image, brandingLocation);
    };
    thumbnail.addEventListener("error", onError, { once: true });
    thumbnail.addEventListener("load", (e) => {
        if ((e.target as HTMLImageElement).naturalWidth === 120) {
            onError();
        }

        thumbnail.removeEventListener("error", onError);
    }, { once: true });

    await displayThumbnail(thumbnail, url, url, false);
}

async function resetToShowLiveCover(videoID: VideoID, thumbnail: HTMLElement,
        image: HTMLImageElement, brandingLocation: BrandingLocation,
        displayThumbnail: (thumbnail: HTMLElement, blobUrl: string | null, remoteUrl: string, removeWidth: boolean) => Promise<void>) {
    
    const videoMetadata = await fetchVideoMetadata(videoID, false);
    if (!videoMetadata.channelID) {
        resetToShowOriginalThumbnail(image, brandingLocation);
        return;
    }

    const url = (await fetchChannelnfo(videoMetadata.channelID, false)).avatarUrl
    if (!url) {
        resetToShowOriginalThumbnail(image, brandingLocation);
        return;
    }

    if (thumbnail.nodeName === "IMG") {
        const newThumbnail = document.createElement("div");
        newThumbnail.className = thumbnail.className;
        newThumbnail.classList.add("cbLiveCover");
        newThumbnail.style.cssText = thumbnail.style.cssText;

        thumbnail.replaceWith(newThumbnail);
        thumbnail.remove();
        thumbnail = newThumbnail;
    }

    const innerImage = document.createElement("img");
    innerImage.classList.add("cb-visible");
    thumbnail.appendChild(innerImage);

    // Ensure errors default back to the original thumbnail
    const onError = () => {
        thumbnail.style.display = "none";
        resetToShowOriginalThumbnail(image, brandingLocation);
    };
    innerImage.addEventListener("error", onError, { once: true });

    innerImage.src = url;
    thumbnail.style.setProperty("--cbThumbBackground", `url("${url}")`);
    await displayThumbnail(thumbnail, null, url, false);
}

function resetToShowOriginalThumbnail(image: HTMLImageElement, brandingLocation: BrandingLocation) {
    image.classList.add("cb-visible");
    image.style.removeProperty("display");

    if (onMobile()
            || brandingLocation === BrandingLocation.Autoplay
            || brandingLocation === BrandingLocation.EndAutonav
            || brandingLocation === BrandingLocation.EmbedSuggestions
            || brandingLocation === BrandingLocation.Related
            || brandingLocation === BrandingLocation.Notification
            || brandingLocation === BrandingLocation.NotificationTitle
            || brandingLocation === BrandingLocation.EndRecommendations
            || !!image.closest("ytd-grid-playlist-renderer")
            || isLiveCover(image)
            || image.parentElement?.classList.contains("ytp-cued-thumbnail-overlay")
            || isOnV3Extension()) {
        hideCanvas(image);
    }

    if (brandingLocation === BrandingLocation.Watch) {
        resetMediaSessionThumbnail();
    }

    resetBackgroundColor(image, brandingLocation);

    if (Config.config!.extensionEnabled 
            && Config.config!.ignoreAbThumbnails) {
        if (image.src) {
            removeAbThumbnail(image);
        } else {
            waitFor(() => !!image.src, 500).then(() => {
                removeAbThumbnail(image);
            }).catch(() => ({}));
        }
    }
}

function removeAbThumbnail(image: HTMLImageElement): void {
    if (image.src?.includes?.("_custom_")) {
        const originalSource = image.src;
        const newSource = image.src
            .replace(/_custom_\d+/, "")
            .replace(/\?.+/, "")
            .replace(/\/\/[^/]+\//, "//i.ytimg.com/");

        const onError = () => {
            if (image.src === newSource) {
                // Reset back to original if it failed
                image.src = originalSource;
            }
        };
        image.addEventListener("error", onError, { once: true });
        image.addEventListener("load", (e) => {
            if ((e.target as HTMLImageElement).naturalWidth === 120) {
                onError();
            }
    
            image.removeEventListener("error", onError);
        }, { once: true });

        image.src = newSource;
    }
}

function resetToBlankThumbnail(image: HTMLImageElement, brandingLocation: BrandingLocation) {
    image.classList.remove("cb-visible");
    image.style.setProperty("display", "none", "important");

    hideCanvas(image);

    if (brandingLocation === BrandingLocation.Watch) {
        setMediaSessionThumbnail("");
    }

    resetBackgroundColor(image, brandingLocation);
}

function resetBackgroundColor(image: HTMLImageElement, brandingLocation: BrandingLocation) {
    if (brandingLocation === BrandingLocation.Related && !onMobile()) {
        const thumbnailElement = image.closest(getThumbnailSelectors()) as HTMLElement;

        if (thumbnailElement) {
            thumbnailElement.classList.remove("thumbnailNoBackground");
        }
    }
}

function removeBackgroundColor(image: HTMLImageElement, brandingLocation: BrandingLocation) {
    if (brandingLocation === BrandingLocation.Related && !onMobile()) {
        const thumbnailElement = image.closest(getThumbnailSelectors()) as HTMLElement;

        if (thumbnailElement) {
            thumbnailElement.classList.add("thumbnailNoBackground");
        }
    }
}

function hideCanvas(image: HTMLElement) {
    const canvas = image.parentElement?.querySelector(".cbCustomThumbnailCanvas") as HTMLCanvasElement | null;
    if (canvas) {
        canvas.style.setProperty("display", "none", "important");
    }
}

function isLiveCover(image: HTMLElement) {
    return !!image.parentElement?.querySelector(".cbLiveCover");
}

export function setupPreRenderedThumbnail(videoID: VideoID, timestamp: number, blob: Blob, width = 1280, height = 720, notifyStopRender = true) {
    const videoCache = thumbnailDataCache.setupCache(videoID);
    const videoObject: RenderedThumbnailVideo = {
        video: null,
        width,
        height,
        rendered: true,
        onReady: [],
        timestamp,
        blob,
        fromThumbnailCache: true
    }
    videoCache.video.push(videoObject);

    if (notifyStopRender) {
        stopRendering(videoID, "Pre-rendered thumbnail");
    }
    
    const unrendered = videoCache.video.filter(v => v.timestamp === timestamp && !v.rendered);
    if (unrendered.length > 0) {
        for (const video of unrendered) {
            (video as RenderedThumbnailVideo).blob = blob;
            video.rendered = true;

            for (const callback of video.onReady) {
                callback(video as RenderedThumbnailVideo);
            }
        }
    }

    for (const failure of videoCache.failures) {
        if (failure.timestamp === timestamp) {
            for (const callback of failure.onReady) {
                callback(videoObject);
            }
        }
    }

    videoCache.failures = videoCache.failures.filter(f => f.timestamp !== timestamp);
}

export function isCachedThumbnailLoaded(videoID: VideoID, timestamp: number): boolean {
    const videoCache = thumbnailDataCache.getFromCache(videoID);
    return videoCache?.video?.some(v => v.timestamp === timestamp && v.rendered && v.fromThumbnailCache) ?? false;
}

async function getAutogeneratedThumbnailUrl(videoID: VideoID): Promise<string> {
    let thumbnailCode = "hq1";
    switch (await getThumbnailFallbackAutoGeneratedOption(videoID)) {
        case 0:
            thumbnailCode = "hq1";
            break;
        case 1:
            thumbnailCode = "hq2";
            break;
        case 2:
            thumbnailCode = "hq3";
            break;
    }

    return `https://i.ytimg.com/vi_webp/${videoID}/${thumbnailCode}.webp`;
}
